﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Manoso.Data.FluentMap
{
    internal interface IMapCommand
    {
        /// <summary>
        /// Insert sql statements of the current command to the given task.
        /// </summary>
        /// <param name="task">Current MapTask data object.</param>
        /// <param name="stack">StringBuilder object holds all parent declares for the current command.</param>
        void GetCommand(IMapTask task, IList<string> stack);
    }

    public interface IMapCommand<T> : IMapSql<T>
    {
        /// <summary>
        /// Define name and alias for the underlining table.
        /// </summary>
        /// <param name="table">Name of the table.</param>
        /// <param name="alias">Alias of the table, it must be unique within the object hierarchy map.</param>
        void Tag(string table, string alias);

        /// <summary>
        /// Specify the store procedure name. Once defined, it will be used for the sql CRUD it belongs to.
        /// </summary>
        /// <param name="name">Store procedure name.</param>
        void Procedure(string name);

        /// <summary>
        /// Define a property/column to be late-bound and will use a value from previous insert or declare commands. 
        /// </summary>
        /// <typeparam name="TP">Type of the property referred.</typeparam>
        /// <param name="propertyLambda">Lambda expression of the referred property, given the T type.</param>
        /// <param name="table">Alias/Name of the table of the referred property.</param>
        /// <param name="referTo">Name of the parameter the property refers to.</param>
        /// <returns></returns>
        void Refer<TP>(Expression<Func<T, TP>> propertyLambda, string table, string referTo);

        /// <summary>
        /// Set the mapping table column to Null for the property if given condition is true. It is ignored if Map methods are used to define mappings.
        /// </summary>
        /// <typeparam name="TP">The property is targeting.</typeparam>
        /// <param name="propertyLambda">Lambda expression of the property, given the T type.</param>
        /// <param name="isNull">Predicate to test, if it is null or returns true, then set the table column to null.</param>
        void Null<TP>(Expression<Func<T, TP>> propertyLambda, Predicate<T> isNull = null);

        /// <summary>
        /// Specify the column name for the primary key of auto generated identity data type.
        /// </summary>
        /// <param name="column">Column name.</param>
        void ID(string column);

        /// <summary>
        /// Specify a property as a Timestamp column.
        /// </summary>
        /// <typeparam name="TP">Type of the property referred.</typeparam>
        /// <param name="column">Column name.</param>
        /// <param name="lambda">Lambda expression of the referred property, given the T type.</param>
        /// <param name="isCheck">Specify whether the timestamp is used to validate sql update, insert or delete.</param>
        void TimeStamp<TP>(string column, Func<T, TP> lambda, bool isCheck = false);
    }

    internal abstract class MapCommand<T> : MapSql<T>, IMapCommand<T>, IMapCommand
    {
        private readonly IList<IMapProperty> _mapReturns = new List<IMapProperty>();
        public IList<IMapProperty> MapReturns
        {
            get { return _mapReturns; }
        }

        protected bool CheckTstamp { get; private set; }

        protected IDictionary<string, Predicate<T>> _nulls = new Dictionary<string, Predicate<T>>();

        #region Public Methods

        public abstract void GetCommand(IMapTask task, IList<string> stack);

        public void Tag(string table, string alias)
        {
            if (_tableAlias == null)
                _tableAlias = Alias;

            Target = table;
            Alias = alias;
        }

        public void Procedure(string name)
        {
            Target = name;
            IsTable = false;
        }

        public void ID(string column)
        {
            var ids = CreateIds();
            AddMaps(ids, true);

            if (Keys != null && Keys.Count > 0)
            {
                var mpKey = Keys.First();
                mpKey.Column.Name = column;
            }
        }

        public void TimeStamp<TP>(string column, Func<T, TP> lambda, bool isCheck = false)
        {
            if (string.IsNullOrWhiteSpace(column) || lambda == null) return;

            var mp = new MapProperty
            {
                HostAlias = TableAlias,
                PropertyType = typeof(TP),
                Column = new MapColumn(column, Alias)
            };

            MapTstamp = mp;
            CheckTstamp = isCheck;

            if (isCheck)
            {
                mp.Load = x => mp.Value = lambda((T)x);
                AddMap(mp, true);
            }
        }

        public void Refer<TP>(Expression<Func<T, TP>> propertyLambda, string table, string referTo)
        {
            if (propertyLambda == null) return;

            var info = propertyLambda.GetMemberInfo<MemberInfo>();
            if (info != null)
            {
                var mp = Keys.Union(Columns).FirstOrDefault(x => x.PropertyName == info.Name) ?? CreateProperty(info);
                if (mp == null) return;

                mp.Column.Reference = new MapColumn(referTo, table).Alias;
                mp.Load = null;
                AddMap(mp);
            }
        }

        public void Null<TP>(Expression<Func<T, TP>> propertyLambda, Predicate<T> isNull)
        {
            var info = propertyLambda.GetMemberInfo<PropertyInfo>();
            if (info != null)
                _nulls.Add(info.Name, isNull);
        }

        #endregion

        #region Protected Methods

        protected void LoadProperties(IMapTask task, bool incId)
        {
            if (Columns.All(x => x.Column.Reference != null))
            {
                var members =
                    ValueType.GetDataMembers(p =>
                    {
                        var dataType = p.GetMemberType();
                        return dataType != null
                            && ((!IsTable || MapTstamp == null || p.Name != MapTstamp.PropertyName)
                            && (dataType.IsValueType || dataType == typeof(string) || dataType == typeof(byte[]))
                            && (incId || Relation.Ids.All(x => x.Name != p.Name))
                            && Columns.All(x => x.PropertyName != p.Name));
                    }).ToArray();
                var maps = CreateProperties(members).ToList();

                AddMaps(maps);
            }

            if (Columns != null)
                foreach (var mp in Columns)
                {
                    var value = task.Values[mp.HostAlias];
                    if (mp.Load != null)
                        mp.Load(value);
                }
        }

        protected void LoadReturns(IMapTask task)
        {
            foreach (var mp in MapReturns)
            {
                mp.Host = task.Values[mp.HostAlias];
            }
        }

        protected override object GetPropertyValue(MemberInfo info, object value)
        {
            if (_nulls.ContainsKey(info.Name))
            {
                var predicate = _nulls[info.Name];
                if (predicate == null || predicate((T)value))
                    return null;
            }
            return base.GetPropertyValue(info, value);
        }
        #endregion
    }
}
